//
// Created by root on 5/30/17.
//

#include "../basic/operator.h"

long gcd(long a, long b) {
    long temp;
    while (b != 0) {
        temp = a % b;
        a = b;
        b = temp;
    }
    return a;
}

static Cell* perform_fraction_reduction(Scheme *sc, long num, long denom) {
    if (num != 0) {
        long the_gcd = gcd(num, denom);
        num = num / the_gcd;
        denom = denom / the_gcd;
    } else {
        //denom = 1;
        return make_integer(sc, 0);
    }
    if (denom < 0) {
        num = -num;
        denom = -denom;
    }
    if (denom == 1) {
        return make_integer(sc, num);
    } else {
        return make_fraction(sc, make_integer(sc, num), make_integer(sc, denom));
    }
}

Cell* fraction_reduction(Scheme *sc, Cell* f) {
    TYPE_CHECK_FOR_CELL(sc, f, FRACTION_TYPE);
    return perform_fraction_reduction(sc, car(f)->_longValue, cdr(f)->_longValue);
}

int is_zero_fraction(Cell *x) {
    return (car(x)->_longValue == 0);
}

Cell* make_fraction(Scheme *sc, Cell* a, Cell* b) {
    TYPE_CHECK_FOR_CELL(sc, a, INTEGER_TYPE);
    TYPE_CHECK_FOR_CELL(sc, b, INTEGER_TYPE);
    long x = a->_longValue;
    long y = b->_longValue;
    if (y == 0) {
        return make_default_exception(sc, "NOT_A_FRACTION", "Denominator can't be 0.");
    }
    if (y < 0) {
        x = -x;
        y = -y;
    }
    long the_gcd = gcd(x, y);
    if (the_gcd == 1 || the_gcd == -1) {
        Cell* fraction = cons(sc, make_integer(sc, x), make_integer(sc, y));
        fraction->_type = FRACTION_TYPE;
        return fraction;
    } else {
        return perform_fraction_reduction(sc, x, y);
    }
}

Cell* fraction_add(Scheme *sc, Cell* x, Cell* y) {
    TYPE_CHECK_FOR_CELL(sc, x, FRACTION_TYPE);
    TYPE_CHECK_FOR_CELL(sc, y, FRACTION_TYPE);
    long a = car(x)->_longValue;
    long b = cdr(x)->_longValue;
    long c = car(y)->_longValue;
    long d = cdr(y)->_longValue;
    long num = (a * d) + (b * c);
    long denom = b * d;
    return perform_fraction_reduction(sc, num, denom);
}

Cell* fraction_mul(Scheme *sc, Cell* x, Cell* y) {
    TYPE_CHECK_FOR_CELL(sc, x, FRACTION_TYPE);
    TYPE_CHECK_FOR_CELL(sc, y, FRACTION_TYPE);
    long a = car(x)->_longValue;
    long b = cdr(x)->_longValue;
    long c = car(y)->_longValue;
    long d = cdr(y)->_longValue;
    long num = a * c;
    long denom = b * d;
    return perform_fraction_reduction(sc, num, denom);
}

Cell* fraction_sub(Scheme *sc, Cell* x, Cell* y) {
    TYPE_CHECK_FOR_CELL(sc, x, FRACTION_TYPE);
    TYPE_CHECK_FOR_CELL(sc, y, FRACTION_TYPE);
    long a = car(x)->_longValue;
    long b = cdr(x)->_longValue;
    long c = car(y)->_longValue;
    long d = cdr(y)->_longValue;
    long num = (a * d) - (b * c);
    long denom = b * d;
    return perform_fraction_reduction(sc, num, denom);
}

Cell* fraction_div(Scheme *sc, Cell* x, Cell* y) {
    TYPE_CHECK_FOR_CELL(sc, x, FRACTION_TYPE);
    TYPE_CHECK_FOR_CELL(sc, y, FRACTION_TYPE);
    if (is_zero_fraction(y)) {
        return make_default_exception(sc, "DIVISION_BY_ZERO", "/: division by zero.");
    }
    long a = car(x)->_longValue;
    long b = cdr(x)->_longValue;
    long c = car(y)->_longValue;
    long d = cdr(y)->_longValue;
    long num = a * d;
    long denom = b * c;
    return perform_fraction_reduction(sc, num, denom);
}

Cell* op_make_fraction(Scheme *sc) {
    LOG_INPUT(sc, "atoms/fraction", "op_make_fraction");

    Cell* x = first(sc->args);
    Cell* y = second(sc->args);
    TYPE_CHECK(sc, x, INTEGER_TYPE);
    TYPE_CHECK(sc, y, INTEGER_TYPE);
    sc->returnValue = make_fraction(sc, x, y);

    LOG_OUTPUT(sc, "atoms/fraction", "op_make_fraction");
    return pop_callstack(sc);
}

Cell* op_numerator(Scheme *sc) {
    Cell* x = first(sc->args);
    TYPE_CHECK(sc, x, FRACTION_TYPE);
    sc->returnValue = car(x);
    return pop_callstack(sc);
}

Cell* op_denominator(Scheme *sc) {
    Cell* x = first(sc->args);
    TYPE_CHECK(sc, x, FRACTION_TYPE);
    sc->returnValue = cdr(x);
    return pop_callstack(sc);
}

Cell* fraction_to_decimal(Scheme *sc, Cell* x) {
    TYPE_CHECK(sc, x, FRACTION_TYPE);
    return make_decimal(sc, (double)(car(x)->_longValue)/(double)(cdr(x)->_longValue));
}

void init_fraction_operators(Scheme *sc) {
    ADD_OPERATOR(sc, "make-fraction", make_fraction);
    ADD_OPERATOR(sc, "num", numerator);
    ADD_OPERATOR(sc, "denom", denominator);
}
